// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Version2Protocol.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Hardware.DAC
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    /// <summary>
    /// Protocol implementation for v2 protocol
    /// </summary>
    public class Version2Protocol : IProtocol
    {
        /// <summary>
        /// Packet header for protocol packet
        /// </summary>
        public const ushort PacketHeader = 0xBEEF;

        /// <summary>
        /// Packet footer for protocol packet
        /// </summary>
        public const ushort PacketFooter = 0xDEAD;

        /// <summary>
        /// The minimum packet size
        /// </summary>
        private const ushort MinimumPacketSize = sizeof(ushort) + // packet start
                                                 sizeof(ushort) + // sequence number
                                                 sizeof(byte) + // command id
                                                 sizeof(byte) + // response code
                                                 sizeof(ushort) + // param length
                                                 sizeof(ushort) + // crc
                                                 sizeof(ushort); // packet end

        /// <summary>
        /// Packet size before payload
        /// </summary>
        private const int PacketPrePayloadSize = 8;

        /// <summary>
        /// Packet size after payload
        /// </summary>
        private const int PacketPostPayloadSize = 4;

        /// <summary>
        /// Index of packet header
        /// </summary>
        private const int PacketHeaderIndex = 0;

        /// <summary>
        /// Index of packet type
        /// </summary>
        private const int PacketTypeIndex = 2;

        /// <summary>
        /// Index of sequence number
        /// </summary>
        private const int PacketSequenceNumberIndex = 3;

        /// <summary>
        /// Index of payload size
        /// </summary>
        private const int PacketPayloadSizeIndex = 5;

        /// <summary>
        /// Index of payload start
        /// </summary>
        private const int PacketPayloadIndex = 7;

        /// <summary>
        /// Index of CRC after the payload
        /// </summary>
        private const int PacketCrcPostPayloadIndex = 0;

        /// <summary>
        /// Index of footer after the payload
        /// </summary>
        private const int PacketFooterPostPayloadIndex = 2;

        /// <summary>
        /// Upper limit timeout of receive waits
        /// </summary>
        private TimeSpan receiveTimeout = TimeSpan.FromMinutes(1);

        /// <summary>
        /// Transport interface
        /// </summary>
        private ITransport transport = null;

        /// <summary>
        /// Current sequence number
        /// </summary>
        private ushort currentSequenceNumber = 1;

        /// <summary>
        /// Sync object
        /// </summary>
        private object syncObject = new object();

        /// <summary>
        /// Time of last sensor update
        /// </summary>
        private DateTime lastSensorUpdate;

        /// <summary>
        /// Initializes a new instance of the Version2Protocol class
        /// </summary>
        /// <param name="transport">Transport to tie to this protocol</param>
        public Version2Protocol(ITransport transport)
        {
            if (transport == null)
            {
                throw new ArgumentNullException("transport");
            }

            this.transport = transport;
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="Version2Protocol"/> class.
        /// </summary>
        ~Version2Protocol()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Gets the DateTime the data was last updated
        /// </summary>
        public DateTime LastSensorUpdate
        {
            get
            {
                return this.lastSensorUpdate;
            }
        }

        /// <summary>
        /// Processes the specified command
        /// </summary>
        /// <param name="command">Command sequence to send</param>
        /// <param name="asyncWrite">The write buffer</param>
        public void RunCommand(ICommand command, AsyncEventArgs asyncWrite)
        {
            if (command == null)
            {
                throw new ArgumentNullException();
            }

            this.GeneratePacket(command, this.currentSequenceNumber, asyncWrite);
            asyncWrite.Transport.AsyncWrite(asyncWrite);
        }

        /// <summary>
        /// Disposes of the current class instance
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Extracts the message data
        /// </summary>
        /// <param name="eventArgs">Incoming event args</param>
        /// <param name="parseMessaage">The delegate to parse the message</param>
        /// <returns>Was the extraction successful</returns>
        public bool ExtractMessageData(AsyncEventArgs eventArgs, ParseMessage parseMessaage)
        {
            this.lastSensorUpdate = DateTime.UtcNow;

            bool corrupted = true;
            byte commandId = 0;
            int parametersLength = 0;
            int parametersOffset = 0;
            if (eventArgs.LastError == null)
            {
                byte[] buffer = eventArgs.Buffer;
                int offset = 0;
                int completed = 0;
                int available = eventArgs.Offset + eventArgs.BytesTransferred;
                corrupted = false;
                while (available - offset >= MinimumPacketSize)
                {
                    if (BitConverter.ToUInt16(buffer, offset) != PacketHeader)
                    {
                        // Skip a byte and continue
                        ++offset;
                        continue;
                    }

                    // Record the start of the message, for any possible crc check
                    int start = offset;

                    // Advance past the header and read the sequence number
                    offset += sizeof(ushort);
                    ushort seqno = BitConverter.ToUInt16(buffer, offset);
                    offset += sizeof(ushort);

                    // Extract the command id and skip the response code
                    commandId = (byte)buffer[offset];
                    offset += sizeof(byte) + sizeof(byte);

                    // Read the length field that indicates the space for params
                    parametersLength = BitConverter.ToUInt16(buffer, offset);
                    offset += sizeof(ushort);

                    // Check we have enough space for the message now we know the length
                    if (available - offset < parametersLength + sizeof(ushort) + sizeof(ushort))
                    {
                        break;
                    }

                    // Skip the params, read the crc and also compute it ourselves
                    parametersOffset = offset;
                    offset += parametersLength;
                    ushort claimedCrc = BitConverter.ToUInt16(buffer, offset);
                    ushort checkedCrc = Crc16.ComputeHash(buffer, start, offset - start);
                    if (claimedCrc != checkedCrc)
                    {
                        corrupted = true;
                        break;
                    }

                    // Check the footer
                    offset += sizeof(ushort);
                    if (BitConverter.ToUInt16(buffer, offset) != PacketFooter)
                    {
                        corrupted = true;
                        break;
                    }

                    // Skip past the footer
                    offset += sizeof(ushort);

                    parseMessaage(buffer, commandId, seqno, parametersOffset, parametersLength);

                    completed = offset;
                }

                int remaining = available - completed;

                if (corrupted) 
                {
                    eventArgs.Offset = 0;
                    eventArgs.Count = 0;
                }
                else if (remaining > 0)
                {
                    Buffer.BlockCopy(buffer, completed, buffer, 0, remaining);
                    eventArgs.Offset = remaining;
                    eventArgs.Count = buffer.Length - remaining;
                }
                else
                {
                    eventArgs.Offset = 0;
                    eventArgs.Count = buffer.Length;
                }
            }

            return !corrupted;
        }

        /// <summary>
        /// Retrieves the next sequence number to use
        /// </summary>
        /// <returns>Sequence number</returns>
        public ushort GetNextSequenceNumber()
        {
            ushort result = 0;

            lock (this.syncObject)
            {
                result = this.currentSequenceNumber;

                if (this.currentSequenceNumber == ushort.MaxValue)
                {
                    this.currentSequenceNumber = 1;
                }
                else
                {
                    this.currentSequenceNumber++;
                }
            }

            return result;
        }

        /// <summary>
        /// Create the packet to go on the wire
        /// </summary>
        /// <param name="command">The command to send</param>
        /// <param name="sequenceNumber">The packet sequence number</param>
        /// <param name="asyncWrite">The write buffer</param>
        private void GeneratePacket(ICommand command, ushort sequenceNumber, AsyncEventArgs asyncWrite)
        {
            MemoryStream ms = null;
            try
            {
                ms = new MemoryStream(asyncWrite.Buffer, asyncWrite.Offset, asyncWrite.Buffer.Length, true);
                using (BinaryWriter bw = new BinaryWriter(ms))
                {
                    ms = null;

                    // Write the header
                    bw.Write(PacketHeader);
                    bw.Write(sequenceNumber);

                    bw.Write(command.CommandType);
                    bw.Write(command.ResponseCode);

                    // Write the command
                    command.WriteRequest(bw);

                    // Write the crc
                    bw.Write(
                        Crc16.ComputeHash(
                            asyncWrite.Buffer, asyncWrite.Offset, (int)bw.BaseStream.Position - asyncWrite.Offset));

                    // Write the footer
                    bw.Write(PacketFooter);

                    asyncWrite.Count = (int)bw.BaseStream.Position;
                }
            }
            finally
            {
                if (ms != null)
                {
                    ms.Dispose();
                }
            }
        }

        /// <summary>
        /// Disposes of the class instance
        /// </summary>
        /// <param name="disposing">True if disposing, false if finalizing</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.transport != null)
                {
                    this.transport.Dispose();
                    this.transport = null;
                }
            }
        }
    }
}
